
package top.kangert.kspider.expression.parsing;

import javax.xml.transform.Source;

/**
 * Wraps a the content of a {@link Source} and handles traversing the contained characters. Manages a current {@link Span} via
 * the {@link #startSpan()} and {@link #endSpan()} methods.
 */
public class CharacterStream {
    private final String source;
    private int index = 0;
    private final int end;

    private int spanStart = 0;

    public CharacterStream(String source) {
        this(source, 0, source.length());
    }

    public CharacterStream(String source, int start, int end) {
        if (start > end) {
            throw new IllegalArgumentException("Start must be <= end.");
        }
        if (start < 0) {
            throw new IndexOutOfBoundsException("Start must be >= 0.");
        }
        if (start > Math.max(0, source.length() - 1)) {
            throw new IndexOutOfBoundsException("Start outside of string.");
        }
        if (end > source.length()) {
            throw new IndexOutOfBoundsException("End outside of string.");
        }

        this.source = source;
        this.index = start;
        this.end = end;
    }

    /**
     * Returns whether there are more characters in the stream
     **/
    public boolean hasMore() {
        return index < end;
    }

    /**
     * Returns the next character without advancing the stream
     **/
    public char peek() {
        if (!hasMore()) {
            throw new RuntimeException("No more characters in stream.");
        }
        return source.charAt(index++);
    }

    /**
     * Returns the next character and advance the stream
     **/
    public char consume() {
        if (!hasMore()) {
            throw new RuntimeException("No more characters in stream.");
        }
        return source.charAt(index++);
    }

    /**
     * Matches the given needle with the next characters. Returns true if the needle is matched, false otherwise. If there's a
     * match and consume is true, the stream is advanced by the needle's length.
     */
    public boolean match(String needle, boolean consume) {
        int needleLength = needle.length();
        if (needleLength + index > end) {
            return false;
        }
        for (int i = 0, j = index; i < needleLength; i++, j++) {
            if (index >= end) {
                return false;
            }
            if (needle.charAt(i) != source.charAt(j)) {
                return false;
            }
        }
        if (consume) {
            index += needleLength;
        }
        return true;
    }

    /**
     * Returns whether the next character is a digit and optionally consumes it.
     **/
    public boolean matchDigit(boolean consume) {
        if (index >= end) {
            return false;
        }
        char c = source.charAt(index);
        if (Character.isDigit(c)) {
            if (consume) {
                index++;
            }
            return true;
        }
        return false;
    }

    /**
     * Returns whether the next character is the start of an identifier and optionally consumes it. Adheres to
     * {@link Character#isJavaIdentifierStart(char)}.
     **/
    public boolean matchIdentifierStart(boolean consume) {
        if (index >= end) {
            return false;
        }
        char c = source.charAt(index);
        if (Character.isJavaIdentifierStart(c) || c == '@') {
            if (consume) {
                index++;
            }
            return true;
        }
        return false;
    }

    /**
     * Returns whether the next character is the start of an identifier and optionally consumes it. Adheres to
     * {@link Character#isJavaIdentifierPart(char)}.
     **/
    public boolean matchIdentifierPart(boolean consume) {
        if (index >= end) {
            return false;
        }
        char c = source.charAt(index);
        if (Character.isJavaIdentifierPart(c)) {
            if (consume) {
                index++;
            }
            return true;
        }
        return false;
    }

    /**
     * Skips any number of successive whitespace characters.
     **/
    public void skipWhiteSpace() {
        while (true) {
            if (index >= end) {
                return;
            }
            char c = source.charAt(index);
            if (c == ' ' || c == '\n' || c == '\r' || c == '\t') {
                index++;
                continue;
            } else {
                break;
            }
        }
    }

    /**
     * Start a new Span at the current stream position. Call {@link #endSpan()} to complete the span.
     **/
    public void startSpan() {
        spanStart = index;
    }

    /**
     * Completes the span started with {@link #startSpan()} at the current stream position.
     **/
    public Span endSpan() {
        return new Span(source, spanStart, index);
    }

    public boolean isSpanEmpty() {
        return spanStart == this.index;
    }

    /**
     * Returns the current character position in the stream.
     **/
    public int getPosition() {
        return index;
    }
}
